Scheme
Scheme è un linguaggio di programmazione funzionale, un dialetto del Lisp di cui mantiene tutte le caratteristiche, che è stato sviluppato negli anni settanta da Guy L. Steele e Gerald Jay Sussman, che lo introdussero nel mondo accademico con una serie di articoli noti come le Lambda Papers e nel libro Structure and Interpretation of Computer Programs, usato per decenni come testo in alcuni esami di Scienze dell'Informazione. In ambiente Gnu/Linux, il desktop manager GNOME incorpora l'interprete Scheme Guile. Programmi di esempio Hello world Il seguente esempio visualizza il testo «Hello world». (display "Hello World!") (newline) Alcuni esempi più complessi Il seguente esempio calcola il massimo comune divisore tra gli argomenti numerici x e y. (define (mcd x y) (cond ((= x y) x) ((> x y) (mcd (- x y) x)) (else (mcd x (- y x))))) Il seguente esempio serve a invertire l'ordine dei caratteri di una stringa; ad esempio, da "abcdef" si ottiene "fedcba". (define (sl w) (substring w (sub1 (string-length w)) (string-length w))) (define (dl w) (substring w 0 (sub1 (string-length w)))) (define (reversem s w) (if (= (string-length s) 0) w (reversem (dl s) (string-append w (sl s))))) Questo invece fa la stessa cosa dell'esempio di sopra, ma usando alcune funzioni di scheme. (define (reverse s) (reversem s "")) ;tail recursion (define (reverse2 w) (list->string(reverse (string->list w))));standard Questa funzione serve a sapere se un numero è primo: (define (primo?m n s) (if (= (sub1 n) s) "e' primo" (if (= 0 (remainder n (add1 s))) (string-append "non e' primo, e' divisibile per: " (number->string (add1 s))) (primo?m n (add1 s))))) (define (primo? n) (primo?m n 1)) Cenni alla programmazione in Scheme A differenza della maggior parte degli altri linguaggi di programmazione, Scheme utilizza una notazione prefissa, ovvero una notazione in cui al posto di scrivere (2 + 3) si scrive (+ 2 3). Questa notazione si propaga a tutte le funzioni, sicché se abbiamo una funzione N-aria f, la sua rappresentazione sarà (f argomento1 argomento2... argomentoN). Tipi di dato Lo Scheme implementa tutti i tipi di dato fondamentale: booleani, numeri, caratteri, stringhe, vettori. Tuttavia include anche tipi speciali, tra cui troviamo le liste (coppie), le porte (flussi di dati), i simboli e le procedure. Per riconoscere questi tipi di dato, Scheme ci mette a disposizione delle particolari funzioni il cui identificatore ha il formato "tipoDiDato?" che restituiscono il booleano TRUE se l'argomento passato è nel formato tipoDiDato. Ecco una tabella riassuntiva: Vediamo ora alcuni dettagli su questi tipi di dati. Booleani Si indicano tramite il carattere cancelletto e la lettera T (TRUE) per vero e F (FALSE) per falso. Quindi #T è vero, mentre #F e falso. Le funzioni sui booleani sono quelle usuali: Numerici Le costanti numeriche si scrivono secondo le usuali regole. Sono disponibili vari tipi di dato numerico: numeri positivi e negativi, numeri con la virgola e numeri in notazione esponenziale. Per riconoscere a che classe di numeri appartiene un'espressione numerale, è possibile usare una delle seguenti funzioni, nuovamente designate da un identificatore con il punto interrogativo, che restituiscono un valore booleano: È inoltre possibile specificare la base di numerazione tramite #fN, dove f è la base ed N un numero: #dN non si usa praticamente mai, visto che la base è di default quella decimale. Esistono poi le seguenti funzioni booleane (punto interrogativo) e di conversione: Per classificare ulteriormente i numeri, ad esempio tra positivi e negativi, o pari e dispari, Scheme mette a disposizione le seguenti primitive: Ovviamente poi, come nella maggioranza dei linguaggi, si hanno a disposizione sia gli operatori aritmetici che molte funzioni matematiche. Caratteri In Scheme i caratteri si indicano con un cancelletto, seguito da un backslash ed il carattere che si vuole esprimere (senza il backslash, ci sarebbe ambiguità tra i caratteri T e F ed i booleani TRUE e FALSE). Tra le funzioni rilevanti troviamo: Di queste funzioni esiste anche la versione ci, case insensitive: si ha quindi char-ci=?, char-ci Le liste sono realizzate come coppie: (2 3) rappresenta un esempio di lista che è evidentemente una coppia; ma anche (2 3 4) è in realtà una coppia, formata dal primo elemento (2) e da tutti gli altri (3 4). A sua volta, l'elemento "tutti gli altri" è una coppia formata dal suo primo elemento (3) e da tutti gli altri (in questo caso solo il 4). La lista è quindi descritta in modo molto naturale in termini ricorsivi. Una lista può contenere qualunque tipo di dato, come caratteri, stringhe, booleani, e anche altre liste (esempio:"(2 3 (4 5))"); come già detto, possono anche contenere tipi di dato misti (esempio:"(#\T 4 (4 #F ("stringa")))"). Le due funzioni fondamentali per agire sulle liste si rifanno alla definizione ricorsiva accennata sopra: abbiamo così la funzione car, che restituisce il primo elemento, e cdr, che restituisce il secondo elemento (l'elemento "tutti gli altri"). Le principali funzioni che Scheme fornisce per le liste sono le seguenti: Notiamo che in questo elenco sono disponibili funzioni aggregate che corrispondono alla composizione di car e cdr: l'espressione (cdr(cdr lst)) può essere scritta in modo più sintetico come cddr(lst), mentre (car (cdr lst)) si può sintetizzare con cadr(lst). Scheme presenta poi altre funzioni composte, alcune delle quali simulano le funzioni sui vettori, ed altre la conversione lista a vettore e viceversa: Funzioni particolari che agiscono sulle liste sono le funzione apply, map e for-each: Per spiegare meglio, facciamo un esempio: > (define lst1 (list 2 3 4 5 6)) > (apply + lst1) > > 20 > (define lst2 (list 1 2 3 4 5)) > (map + lst1 lst2) > > (3 5 7 9 11) Vettori I vettori sono degli array standard, ovvero sono una struttura che contiene un solo tipo di dato. Si rappresentano come liste con un cancelletto prefisso, ovvero #(el1 el2... elN), dove el1 ha indice 0, ed elN ha indice N-1. Le funzioni che Scheme fornisce per la gestione dei vettori sono le seguenti: Porte Le porte sono oggetti che rappresentano flussi di dati. Sulle porte sono disponibili numerose funzioni, tra le quali troviamo la lettura/scrittura dallo standard input/output (lettura/scrittura di caratteri da/nel terminale, analogamente alle funzioni printf e scanf nel linguaggio C) e la gestione di file. Le porte si possono aprire in input (lettura di dati) ed in output (scrittura di dati). Alcune delle funzioni fornite dallo Scheme sono le seguenti: Lettura dei dati Le letture di dati, dette "operazioni di input", avvengono tramite le seguenti funzioni: Nota: se su read-char e read non si specifica la porta port, la lettura avviene dallo standard input (lettura da console). Scrittura dei dati Le scritture di dati, dette "operazioni di output", avvengono tramite le seguenti funzioni: Nota: se su write-char e write non si specifica la porta port, la scrittura avviene sullo standard output (scrittura su console). Definizione di procedure Definizione di un dato In Scheme la definizione di un dato di qualsiasi tipo avviene tramite la funzione define: Definizione di un valore numerico: > (define mio_numero 3) Definizione di una stringa: > (define mia_stringa "Ciao Mondo") Definizione di una lista: > (define mia_lista (list #T 6 2 #/G)) Anche per le procedure si usa quindi define. Il metodo più semplice per creare una procedura che prenda un certo numero di argomenti ha la struttura seguente: > (define (mia_procedura arg1 arg2... argN) ...) In Scheme è disponibile la funzione lambda per creare procedure senza nome. Essa ci fornisce una modalità alternativa per definire una procedura, creando una procedura anonima e assegnandola, come se fosse un valore convenzionale, a una variabile. Definizione di una procedura, tramite la funzione lambda: > (define mia_procedura (lambda (arg1 arg2... argN) ...)) Lambda permette anche la definizione di procedure all'interno di altre procedure. Costrutti fondamentali Condizione semplice (if) Dopo aver visto i tipi di dato e le funzioni per operare su di essi, e dopo aver capito come si definisce una procedura, passiamo ai costrutti fondamentali. Il costrutto if si presenta nel seguente modo: > (if condizione espressione_nel_caso_la_condizione_sia_vera eventuale_espressione_nel_caso_la_condizione_sia_falsa) ;dice se l'argomento fornito e' uguale o diverso da 5 (define (prova_if arg1) (if (= arg1 5) "l'argomento fornito e' 5" "l'argomento fornito e' diverso da 5")) Si potrebbe cioè dire che l'if di Scheme contiene in sé anche l'else degli altri linguaggi di programmazione. Condizione composta (cond) È equivalente a una catena di multipli if, e si presenta nel seguente modo: > (cond (prima_condizione espressione) (seconda_condizione espressione) ... (else espressione)) Ci sono un paio di cose da notare: # l'else non è obbligatorio: se le condizioni esplocitate sono esaustive, cioè coprono il 100% dei casi possibili, si può omettere. Al contrario, in assenza di else, nel caso in cui nessun caso è verificato si genererà un errore di Run-time. # Si tratta di una funzione derivata, implementabile tramite l'if ed il costrutto begin. Condizione basata sul valore restituito da un'espressione (case) Si tratta di una espressione soggetta a una condizione, dal cui risultato dipende il valore assunto dall'espressione stessa. I possibili valori sono specificati nella struttura come val1, val2, val3: > (case espressione_che_viene_valutata ((val1) espressione) ((val2) espressione) ... (else espressione)) Come nel caso del costrutto cond, l'else è opzionale. Blocchi di espressioni (begin) Per esprimere un blocco di espressioni in Scheme si usa la funzione begin: > (begin prima_espressione seconda_espressione ... n-esima_espressione ) Questo permette di specificare più espressioni (quelle racchiuse dal begin) in un punto dove sintatticamente occorre una espressione unica. Un esempio tipico è negli if. Un costrutto non fondamentale (do) Essendo un linguaggio funzionale, in Scheme sarebbe bello poter usare sempre l'if e la ricorsione. Per quanto sia possibile nella teoria, questa soluzione non sempre aiuta la leggibilità, né dà luogo necessariamente alla soluzione algoritmicamente più efficiente o naturale. Scheme mette quindi a disposizione il costrutto do, che consente di eseguire cicli. In questo, il costrutto do svolge un ruolo simile ai for e ai while di altri linguaggi di programmazione. Il do si presenta sotto diverse forme. La prima, che assomiglia al costrutto do-until di java, è la seguente: > (do () (condizione_di_uscita eventuale_espressione_da_eseguire_prima_dell_uscita) prima_espressione_del_ciclo seconda_espressione_del_ciclo ... n-esima_espressione_del_ciclo ) Si guarda la condizione di uscita: se è vera vengono valutate sequenzialmente le espressioni successive (prima_espressione_del_ciclo, seconda_espressione_del_ciclo... n-esima_espressione_del_ciclo). IL'esecuzione viene quindi iterata, con un nuovo controllo della condizione di uscita. Quando questa fallisce, viene eseguita l'espressione (opzionale) che la segue, e poi il ciclo termina. La seconda forma assomiglia molto ai generici costrutti for: > (do ((variabile valore_iniziale_della_variabile espressione_di_aggiornamento)) (condizione_di_uscita eventuale_espressione_da_eseguire_prima_dell_uscita) prima_espressione_del_ciclo seconda_espressione_del_ciclo ... n-esima_espressione_del_ciclo ) La variabile viene inizializzata ad un certo valore; ad ogni iterazione successiva, la variabile viene aggiornata eseguendo l'espressione_di_aggiornamento, che può prevedere un incremento o decremento della variabile, ma anche un'operazione più generale. Per ogni altro aspetto l'esecuzione avviene come nel caso precedente, dove tuttavia condizione_di_uscita viene tipicamente soddisfatta a seconda dei valori assunti dalla variabile di ciclo. ;esempio che visualizza i numeri naturali inferiori alla variabile numero (define (prova_do numero) (do ((indice 0 (+ indice 1))) ((>= indice numero)) (display indice) (newline) )) Programmare in Scheme Come già notato, Scheme è particolarmente adatto a esprimere gli algoritmi in forma ricorsiva. La ricorsione semplice si ottiene richiamando un'unica volta la procedura stessa. Supponiamo ad esempio di non avere a disposizione l'operatore *, e di voler definire la moltiplicazione tra interi per addizioni successive. Visto che k*n equivale a k+k+...+k, con k ripetuto n volte, allora potremo scrivere il seguente codice: (define (molt a b) (if (= a 0) 0 (+ b (molt (- a 1) b)))) In questo esempio la funzione molt viene richiamata all'interno del suo stesso codice. Come funziona la ricorsione? Supponiamo di avere (molt 3 4): il risultato atteso è 3*4=12. Vediamo da vicino il flusso di esecuzione: > (molt 3 4) iterazione: ricorsione (+ 4 (molt (- 3 1) 4)) iterazione: ricorsione (+ 4 (+ 4 (molt (- 2 1) 4))) iterazione: ricorsione (+ 4 (+ 4 (+ 4 (molt (- 1 1) 4)))) iterazione: si è nella condizione in cui a è uguale a 0, si sostituisce (molt 0 4) con 0 (+ 4 (+ 4 (+ 4 0))) risoluzione (+ 4 (+ 4 4)) risoluzione (+ 4 8) del risultato 12 Un secondo tipo di ricorsione prevede una ricorsione che ad ogni iterazione può eseguire chiamate diverse, a seconda delle condizioni che si verificano. Vediamone un esempio tramite fibonacci. (define (fibonacci n) (cond ((= n 1) 1) ((= n 2) 2) (else (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))) Anche se è possibile descrivere il flusso d'esecuzione come sopra, questo cresce molto in fretta (vedere la Teoria della complessità computazionale), e quindi non verrà riportato. Esempi vari di programmazione in Scheme Un esempio di programma che interagisce con l'utente: (define (maxpot b n k) (if (not (>= n (expt b k))) (sub1 k) (maxpot b n (add1 k)))) (define b 0) (define n 0) (quote "Ora sara' calcolata la massima potenza per b^k <= n") (quote "Inserisci ora b:") (set! b (read)) (quote "Inserisci ora n:") (set! n (read)) (if (and (> b 1) (> n 0)) (string-append "La massima potenza che soddisfa b^k <= n e: " (number->string (maxpot b n 0))) (quote "E' subentrato un errore: devi aver imposto b<=1 e/o n<=0")) Implementazione del problema del tavolo rotondo (o di Cesare): (define (aski s);position in A.S.C.I.I. (char->integer (string-ref s 0))) (define (retro s);from A.S.C.I.I. to string (list->string (list (integer->char s)))) (define (dl s) ;delete last "char" (substring s 0 (- (string-length s) 1))) (define (sl s) ;select last "char" (substring s (- (string-length s) 1) (string-length s))) (define (lwc? s) (and (> (aski s) 96) (< (aski s) 123))) ;lower-case? (define (upc? s) (and (> (aski s) 64) (< (aski s) 91))) ;upper-case? (define (modGC old n new) ;modulo gcesare (define (h s) (modGC (dl old) n (string-append (retro s) new)));applicazione principale tail-recursion ; (aski (sl old)) è l'unità carattere in numero, sarebbe un bene rinominarla!, con un let ch ad esempio, ma dove? così come (sl old)... ; la seconda e la terza condizione andrebbero riunite in una sola a cui vanno cambiati due caratteri..min (65 o 97) e max (90 o 122) (cond ((string=? "" old) new) ;condizione di uscita ((and (lwc? (sl old)) (> (+ n (aski (sl old))) 122)) (h (+ 97 (- (sub1 n) (- 122 (aski (sl old)))))));gestione del riporto ((and (upc? (sl old)) (> (+ n (aski (sl old))) 90)) (h (+ 65 (- (sub1 n) (- 90 (aski (sl old))))))) ((not (or (lwc? (sl old)) (upc? (sl old)))) (h (aski (sl old))));eccezioni:numeri, spazi bianchi ecc. (else (h (+ n (aski (sl old))))))); siamo nella normalita', l'alfabeto non deve nemmeno cominciare dall'inizio (define (gcesare old n w) (if (and (< n 26) (> n 0)) (cond ((string=? w "cod") (modGC old n "")) ((string=? w "dec") (modGC old (- 26 n) "")) (else "opzione non valida, riprovare con dec o cod")) "n deve essere un numero, compreso tra 0 e 26, estremi esclusi!")) Un esempio più complesso, il gioco Tic Tac Toe (semplice implementazione ASCII): (define n 0) ;input (define (slst? l) (if (null? l) #t (if (number? (car l)) #f (slst? (cdr l))))) ;symbolic list? (define (view l) (begin (display "La situazione attuale e' la seguente: \n") ;Visualizzatore ^_^ (display (cons (car l) (cons (cadr l) (list (caddr l))))) (newline) (display (cons (cadddr l) (cons (cadddr (cdr l)) (list(cadddr (cddr l)))))) (newline) (display (cdddr(cdddr l))) (newline))) (define (free? l e) (if (null? l) #f (if (not (equal? (car l) e)) (free? (cdr l) e) #t))) (define (tr l e s) ;trova e rimpiazza (if (equal? (car l) e) (cons s (cdr l)) (cons (car l) (tr (cdr l) e s)))) (define (win l wpos) (cond ((null? wpos) #f) ; posizione vincente? ->vai a win?.. ((eqc? (car wpos) l) #t) (else (win l (cdr wpos))))) (define (win? l)(win l '((1 2 3) (4 5 6) (7 8 9) (1 4 7) (2 5 8) (3 6 9) (1 5 9) (7 5 3)))) ;posizioni vincenti (define (eqc? l1 l2) (if (null? l1) #t (if (cc l2 (car l1)) (eqc? (cdr l1) l2) #f))) ;gli elementi della lista compaiono nella lista ? (define (cc l e) (if (null? l) #f (if (equal? (car l) e) #t (cc (cdr l) e)))) ; e' contenuto nella lista ? (define (wplay genlst plslst mnslst turn) (let ((wis '(display "Ha vinto il giocatore: "))) (view genlst) (cond ((win? plslst) (eval wis) (display "+")) ((win? mnslst) (eval wis) (display "-")) ((slst? genlst) (display "Parita', non ha vinto nessuno!")) (else (begin (display "Inserisci un numero,da 1 a 9, e' il tuo turno ") (display turn) (newline) (set! n (read)) (display "Hai inserito: ") (display n) (newline) (if (number? n) (if (< n 10) (if (free? genlst n) (equal? turn '+) (wplay (tr genlst n '+) (append plslst (list n)) mnslst '-) (wplay (tr genlst n '-) plslst (append mnslst (list n)) '+) (begin (display "Il posto inserito e' occupato\n") (wplay genlst plslst mnslst turn))) (begin (display "Il posto inserito e' inesistente\n")(wplay genlst plslst mnslst turn))) (begin (display "Il posto e' indicato tramite un numero da 1 a 9\n")(wplay genlst plslst mnslst turn)))))))) (define play (begin (display "Inizio del gioco, comincia + .\n")(wplay (list 1 2 3 4 5 6 7 8 9) (list '+) (list '-) '+))) Altri progetti Collegamenti esterni Implementazioni * Drscheme interprete Open Source per tutti i sistemi operativi * Chez Scheme interprete freeware per Microsoft Windows e Unix * Chicken compilatore Scheme-to-C * Bigloo compilatore Scheme-to-C e Scheme-to-Java compiler * Kawa ambiente Scheme scritto in Java, che compila codice Scheme in bytecode Java Altre risorse * A large collection of Scheme resources * Scheme Requests for Implementation (SRFI) * How to Design Programs * Una bibliografia sulla ricerca correlata a Scheme, con link a molti articoli accademici, comprese le originali Lambda Papers Categoria:Linguaggi di programmazione